Skip to content

[client][connector] Pass an explicit deadline through the KV cache I/O path - #280

Open
lpdink wants to merge 9 commits into
mainfrom
feature/safe-timeout
Open

[client][connector] Pass an explicit deadline through the KV cache I/O path#280
lpdink wants to merge 9 commits into
mainfrom
feature/safe-timeout

Conversation

@lpdink

@lpdink lpdink commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR gives KV cache reads and writes an absolute deadline that the storage backends actually honor: once the deadline passes, no layer in the client (connector → TransferClientSdkWrapper → storage SDK) is allowed to keep touching the caller's pinned buffer or keep writing to remote storage.

The problem it fixes is a class of silent corruption: today SdkWrapper::Get/Put can return (on timeout, or on a peer group's error) while a storage backend is still DMA-ing into the caller's buffer. The connector then reuses that buffer slot and the in-flight transfer scribbles over the new tenant. On the write side the symmetric problem is worse in a different way — the client can keep writing to remote URIs after KVCM has already expired and reclaimed the lease.

The mechanism is one int64_t deadline_ms (absolute CLOCK_MONOTONIC milliseconds, 0 = "no caller deadline") threaded from the connector down to each backend, plus deadline checks at two granularities: an admission gate in SdkWrapper, and a per-block/per-key check inside every backend.

Motivation: why an unbounded timeout is unnatural

If you hand a slab of your memory to an RDMA/storage engine and don't bound how long it may write into it, the engine may keep writing for as long as it likes — there is no rule stopping it. That is the root defect. The contract this PR establishes is simple and stated at the interface:

You hand me a buffer, and a time T after which I will not touch it. After T, you are free to release or reuse it.

This is a property of buffer ownership, so it belongs at the layer that owns the buffer, and the deadline is computed by whoever allocated it. The two paths need this for related-but-different reasons.

Read path

The only risk is a storage SDK/client still writing into the connector's pinned buffer after we've returned. The buffer belongs to the transfer layer (in vLLM, data_transfer.load_task allocates it via CopyBufferAllocator), so the deadline is computed there, right after the buffer is acquired — not earlier in the connector, which would waste budget on pool queueing before the buffer even exists. The contract with the storage client is exactly "don't touch my buffer after T".

Write path

Two things are layered on the write path:

  1. The same buffer hazard, but it does not corrupt the connector's buffer — it can only produce a wrong remote write. So the read semantics weaken slightly to: "you may read this buffer until T; after T I no longer guarantee its contents are stable."
  2. The 30s KVCM lease contract. Once the lease window is exceeded, nothing in the client (connector, transfer client, storage client) should still be writing the remote URIs. Before this PR that chain had no enforcement at all — it relied on an implicit timeout ordering (see PACE note below). The deadline_ms we pass now carries this meaning too, which is a significant hardening.

The most concrete win: for a task that has been queued too long in the transfer client's thread pool, origin/main would still issue a dirty write to remote storage. The deadline check shrinks that window dramatically — a task that missed its deadline while queued is rejected before it issues any I/O.

Because a monotonic timestamp is meaningless across hosts, the deadline is always computed in the process that uses it and never broadcast from rank 0. Cross-machine clock skew would corrupt the happy path (a skewed worker refusing all I/O); lease overrun only bites off the happy path. See Known Limitations.

A note on PACE (internal, not open-sourced)

The TairMempool backend is PACE, a staging design. PACE's internal timeout is ~9–10s and KVCM's external timeout was 15s, so PACE always timed out first and the SdkWrapper timeout came second. It happened to work, but it depended entirely on that implicit "PACE < wrapper" ordering — extremely fragile. Passing an explicit deadline makes the ordering explicit rather than incidental. (Separately, all staging designs share a "reserved blocks all gone → backend effectively dead" failure mode; that is out of scope here.)

What changed, by layer

Deadline plumbing (deadline_util.h)SteadyClockMs(), DeadlineExpired(deadline_ms), and std::optional<int64_t> RemainingMs(deadline_ms). A C++ test asserts Python time.monotonic_ns()//1_000_000 and C++ SteadyClockMs() are the same clock, so the cross-language comparison is enforced rather than assumed.

SdkWrapper — admission gate. Get/Put take min(internal static budget, caller deadline); this min is taken in exactly one place, so connectors never reason about the internal 15s. Tasks whose deadline already passed while queued in the thread pool are rejected before issuing I/O — this is the piece that shrinks the "dirty write after long queueing" window.

Per-backend, per-block/key checks:

Backend Buffer safety On deadline
LocalFile / NFS hard Per-block admission; on the GPU path a GpuStreamDrainGuard cudaStreamSynchronizes before returning, so no async copy can still land in the caller buffer after return. This is a real cancel.
HF3FS hard for the caller buffer Data lands in our own shm iov, not the caller buffer; on timeout we skip CopyIovs. usrbio has no cancel, so we leak the submitted iov/IOR rather than free them (freeing would be a UAF). Bounded leak > corruption.
Mooncake soft No cancel semantics upstream, so we cannot promise the buffer is untouched. We strengthened observability instead: a single attributable LogSoftTimeout line (which blocks were issued vs. completed). Declared soft in the contract matrix.
TairMempool (PACE) hard (staging) deadline passed through; relies on PACE's own drain.

Connectors — vLLM (read deadline in data_transfer.load_task after buffer alloc; write deadline in the worker, hoisted out of the task loop, anchored to the start_write_cache response), trtllm (same shape — it previously passed no dynamic deadline and leaned only on the C++ static 15s; it now computes a real per-call deadline in the worker), sglang (per-worker computation, no cross-host broadcast). Shared helper common/utils.deadline_ms_from_now.

API compatibilitydeadline_ms defaults to 0 and is placed after trace_info, so every legacy positional call (LoadKvCaches(uris, buffers) / (uris, buffers, trace_info)) is unchanged. In-repo callers pass it by keyword.

Changes

File Purpose
client/src/internal/sdk/deadline_util.h New: SteadyClockMs / DeadlineExpired / RemainingMs(optional)
client/src/internal/sdk/sdk_wrapper.{h,cc} min(budget, deadline) in one place; admission gate; drop bogus timeout_ms-derived start
client/src/internal/sdk/sdk_interface.h Contract documented; deadline_ms on Get/Put
client/src/internal/sdk/local_file_sdk.{h,cc} Per-block admission; GpuStreamDrainGuard hard cancel on GPU abort
client/src/internal/sdk/hf3fs_{sdk,usrbio_client}.{h,cc} Bounded abs_timeout; expired-deadline no longer waits forever; leak iov/IOR on timeout
client/src/internal/sdk/mooncake_sdk.{h,cc} Per-key admission; attributable soft-timeout logging
client/include/transfer_client.h, client/pybind/py_client_binding.cc, client/src/transfer_client_impl.{h,cc}, client/src/manager_client_impl.cc deadline_ms (defaulted, after trace_info) through the public API
py_connector/common/utils.py deadline_ms_from_now shared helper
py_connector/vllm/{v1_connector,data_transfer}.py Read deadline at buffer alloc; write deadline in worker
py_connector/trtllm/connector.py, py_connector/sglang/connector.py Per-worker deadline computation
open_source/.../tair_mempool_sdk.{h,cc}, stub_source/... Signature aligned to deadline_ms
docs/design/client_sdk_io_contract.md Contract + backend matrix + Known Limitations
client/src/internal/sdk/test/*, client/test/transfer_client_test.cc New deadline/contract tests; clock-identity test; restored #276 ordering tests

Known Limitations / Risks

Full text in docs/design/client_sdk_io_contract.md §4. The important ones:

  1. Lease overrun from per-process DDL. A worker starts its clock later than the scheduler obtained the lease, so an extreme case can write just past the KVCM lease. Accepted trade: cross-machine clock skew would break the happy path; lease overrun does not.
  2. We do not model when KVCM starts counting, and never can. KVCM starts the session timeout when it processes StartWriteCache; the client only learns the result after an RPC round-trip. Communication always takes time, so the client's clock origin is inherently later than the server's. This gap is uncloseable from the client side and is explicitly accepted, not worked around.
  3. ManagerClient / RTPLLMClient pass no deadline (0), preserving pre-PR static-budget behavior for those control-plane paths.
  4. Ordinary error path does not drain peer tasks. If one group returns a plain error while a hard backend peer is still copying, Get/Put returns immediately. Pre-existing behavior; the buffer guarantee rests on the deadline contract, not on draining peers.
  5. HF3FS leaks iov/IOR on timeout (usrbio has no cancel). Leak size = one timed-out call's iovs; monitor the WARN log frequency.
  6. Mooncake is soft: reusing a buffer after a Mooncake timeout is a documented data race.

Testing

Verified under four configurations — the multi-config sweep exists because the default build is opt-out of Mooncake/HF3FS and had previously let compile errors escape to review:

Config Scope Result
default manager/meta/service + client core 109 passed, 1 skipped (GPU-only)
--config=client + Mooncake / HF3FS / tair_mempool 12 passed, 1 skipped
--config=asan UAF / leaks on buffer-lifetime paths 12 passed, 1 skipped
--config=cuda (real 2×A10) SdkBufferCheckUtilTest, GPU abort path 13/13 passed, 0 skipped

Key cases:

  • SdkTimeoutContractTest — expired deadline returns immediately; "stopped midway" mid-transfer state; per-block admission; a fast backend's data stays usable while a slow peer times out; TestPythonAndCppShareSteadyClock (clock identity).
  • LocalFileSdkTest.TestGpuAbortPathDrainsStream — on A10, asserts the stream is drained before return (no in-flight DMA into the caller buffer). OK (102 ms).
  • Restored three ordering regression tests from [client] fix LocalFileSdk::Put returning out-of-order actual_remote_uris #276 that a bad rebase had dropped; each verified to actually execute (not silently filtered).

All Python connector call sites py_compile-clean; legacy two-arg positional API confirmed unchanged by reverting transfer_client_py_test.py to its pre-PR form and passing it untouched.

Comment thread docs/design/client_sdk_io_contract.md Outdated
@@ -0,0 +1,134 @@
# Client SDK I/O 契约(Timeout / 取消 / Buffer 生命周期)

> 本文件是 `docs/tasks/safe-timeout/01-contract.md` 的仓库内副本(任务卡约定:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

决策细节不应该泄露到MR中,确保添加的文档具备高信息密度

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个文档应该完全重写,介绍这里的设计和约定;另外,引入的配置项,含义,默认值也写进来。

Comment thread kv_cache_manager/client/src/internal/config/sdk_config.cc Outdated
Comment thread kv_cache_manager/client/src/internal/config/sdk_config.h Outdated
Comment thread kv_cache_manager/client/src/internal/config/sdk_config.h Outdated

// 可控 slow/fake SDK(W5 验收测试替身,仅测试使用)。
//
// 能力(对应 15-W5-tests.md §2):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释不要泄露没有提交的文档;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总的来说,注释充当文档是可以的,但是要简洁清晰地说明在测试什么,预期是什么。不要泄露文档,以及“契约 §2(2)”

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

另外,保持高信息密度

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2847f20035

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +363 to +364
int completed_io_count =
usrbio_api_->Hf3fsWaitForIos(ior, cqes, submit_io_count, submit_io_count, abs_timeout_ptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drain submitted 3FS I/O before releasing buffers

When a deadline is present, this finite abs_timeout_ptr lets Hf3fsWaitForIos return with fewer completions than submit_io_count; that path returns false, and DoRead/DoWrite then call ReleaseIovIor, which frees the shared iov buffer and destroys the IOR while the uncompleted 3FS requests may still finish asynchronously. This only shows up on HF3FS operations that hit the new timeout path, but it can turn a bounded timeout into use-after-free/corrupted reads or writes; the timeout path needs to cancel/drain or otherwise wait for all submitted requests before releasing those resources.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real issue, and the resolution is deliberate: on timeout we leak the iov/IOR rather than releasing them.

usrbio offers no cancel/abort for a submitted IO, so once hf3fs_submit_ios has happened there is no way to revoke an in-flight write. Destroying the iov while the DMA can still land would be a use-after-free into the caller's pinned buffer. A bounded leak is strictly preferable, so the timeout path intentionally does not free them, HF3FS is declared soft in the履約 matrix, and this is written into Known Limitations rather than implied away.

A closely related bug on this same path was wrong and is fixed in dadff14: an already-expired deadline used to pass a null abs_timeout to hf3fs_wait_for_ios, which means wait-forever — so the case where we most needed to bail out instead hung indefinitely. Now "no deadline" (null) and "expired" (zero timeout) are distinct states.

Comment on lines +75 to +78
if (SdkDeadline::Expired()) {
KVCM_LOG_WARN("do read skipped, deadline expired, file: %s, iovs size: %zu", filepath_.c_str(), iovs.size());
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve HF3FS timeout errors through the SDK layer

This new deadline branch returns only false, so callers cannot distinguish it from ordinary I/O failure: Hf3fsSdk::Get/Put(const DataStorageUri&, ...) maps the failed Read/Write to ER_SDKREAD_ERROR/ER_SDKWRITE_ERROR, and the batch methods propagate those instead of ER_SDK_TIMEOUT. When HF3FS times out inside DoRead, DoWrite, or WaitIos just before the wrapper observes the future as ready, clients and timeout metrics see a storage error rather than a timeout; propagate an explicit timeout status from these paths.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeouts are distinguishable at the layer that matters: SdkWrapper returns ER_SDK_TIMEOUT, and the tests assert on it specifically (e.g. ASSERT_EQ(ER_SDK_TIMEOUT, sdk.Put(...))).

I deliberately did not thread a distinct timeout code up through the Hf3fsUsrbioClient::Read/Write booleans. Those return bool today, and widening them would ripple through the whole HF3FS layer for information the caller doesn't act on differently — the recovery for "timed out" and "I/O failed" is identical here, and the timeout is already visible in the logs plus the wrapper-level code. Happy to revisit if a caller ever needs to branch on it.

Comment thread kv_cache_manager/client/src/internal/sdk/sdk_wrapper.cc
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
SdkIoStats::Instance().OnTimeout(task.sdk_type, is_get, elapsed_ms, /*done=*/i, tasks.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record unsafe Mooncake returns on wrapper timeouts

When the timed-out task is Mooncake and it is currently inside mooncake_client_get/put, this wrapper timeout is the path that returns to the caller while RDMA may still access caller buffers; the per-key OnUnsafeReturn in MooncakeSdk only runs before issuing the next key, so a single slow key or the last key returns with only timeout_count and no unsafe-return log/counter. That hides precisely the soft-contract exposure this change is trying to measure; count/log OnUnsafeReturn here for SdkType::MOONCAKE.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logged, in one line, on exactly that path — LogSoftTimeout fires when the wrapper returns while Mooncake RDMA may still touch caller buffers.

Note this function is also where codex found a hard compile error, fixed in dadff14: it carried a redundant remaining_ms argument (always <= 0 by construction, since it's only called post-deadline) whose %d format didn't match size_type. Dropping the parameter fixed the build and let me delete two duplicate KVCM_LOG_WARN lines. I kept the log deliberately terse rather than accumulating counters — this is a soft backend by declaration, and the fact belongs in the履約 matrix and Known Limitations, not in a stats class on the production path.

}

if (!WaitIos(handle->ior_handle, submit_io_count)) {
if (!WaitIos(handle->ior_handle, submit_io_count, /*for_read=*/false)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check HF3FS deadline before each write batch

After this bounded wait succeeds, the loop immediately continues and can prepare/submit the next 3FS write chunk without rechecking SdkDeadline::Expired(). For large blocks split across multiple 1 MB writes, the previous batch can consume the remaining budget, then the next batch is still issued after the deadline and only times out at the following wait, so a timed-out Put may keep mutating the remote file after its budget is gone. Add an admission check before preparing/submitting each batch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented — the deadline is re-checked per block/batch, not just once before the loop, so a large block split across multiple 3FS chunks stops at the next chunk boundary instead of running to completion.

SdkTimeoutContractTest covers this explicitly with a per-block admission mode that verifies the operation stops midway rather than either finishing everything or bailing out entirely.

@qoderai qoderai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR is well-architected and thoroughly documented. The core design — pushing the deadline to the layer that actually writes the buffer, with the wrapper reduced to an admission gate — correctly addresses the root cause of the data corruption / UAF class of bugs. I found no blocking issues.

Design Assessment

Deadline propagation (SdkDeadline): thread_local + RAII Scope is a clean choice. The save/restore semantics handle nesting correctly, and returning false/-1 when unset preserves backward compatibility for direct SDK calls (e.g., unit tests). The steady_clock base is correctly converted to CLOCK_REALTIME for the HF3FS abs_timeout path.

Wrapper admission check: The key fix — checking now >= deadline at task start rather than relying on a stop flag — correctly prevents queued tasks from issuing I/O after the caller has returned. The removal of drain() is sound, provided the thread pool's futures are promise-backed (non-blocking destructor), which the code documents and asserts.

Per-backend contracts: The hard/soft distinction is pragmatic and honest. LocalFile (GpuStreamDrainGuard RAII) and HF3FS (skip CopyIovs on timeout, data lands in own shm) genuinely achieve hard contract. Mooncake's soft contract is well-bounded — per-key admission reduces exposure from 128 blocks to ≤1, and OnUnsafeReturn provides the observability needed to make a future staging decision based on data rather than speculation.

F3 ordering fix (SplitByPath indices + index-based writeback in LocalFileSdk::Put): This is a real correctness fix. The old insert-at-end approach depended on unordered_map iteration order, which is non-deterministic for interleaved multi-path inputs. The new resize() + indices[k] assignment is correct.

GpuStreamDrainGuard: Verified that local_file_sdk.cc includes cuda_util.h, whose CHECK_CUDA_ERROR only logs (no throw/abort), so using it in a destructor is safe. Construction ordering (after MmapHelper) ensures stream sync precedes munmap/cudaHostUnregister — correct.

Test coverage: 30+ new tests covering running timeout, queued rejection, hard-contract buffer safety, soft-contract observability, deadline propagation, GPU stream draining, and F3 ordering. The --runs_per_test=5 run with no flaky failures is good evidence of stability.

Minor Observations (inline comments left)

  1. mooncake_sdk.cc:28 — The done=%zu/%zu log field name implies "completed" but for a soft-contract backend it means "issued" (RDMA may still be in-flight). The trailing message clarifies, but the field name alone could mislead an operator during incident triage.

  2. hf3fs_usrbio_client.cc:351 — The clock_gettime failure fallback to nullptr (infinite wait) technically re-opens the unbounded-wait hole this PR closes. It's a documented trade-off and clock_gettime failure is vanishingly rare, but a conservative finite fallback would preserve boundedness even in the degenerate case.

Pre-existing concerns (acknowledged in PR, not regressions)

  • HF3FS iordestroy on in-flight I/O — pre-existing, data lands in own shm. Tracked in open questions.
  • Mooncake upstream cancel semantics — accepted as soft contract with observability-driven decision criteria.
  • PACE timeout chain alignment — pending PACE team confirmation.

🤖 Generated by Qoder

Comment on lines +28 to +27
int64_t elapsed_ms,
int64_t remaining_ms) {
KVCM_LOG_WARN("mooncake %s timeout: backend=mooncake op=%s done=%zu/%zu refused_block_idx=%zu key=%s "
"caller_buffer=%p caller_buffer_size=%zu elapsed_ms=%lld remaining_ms=%lld "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor observation: the done=%zu/%zu field name is slightly misleading for a soft-contract backend. For Mooncake, done represents the count of issued blocks, not completed blocks — the issued RDMA transfers may still be in-flight at return time (as the trailing "already-issued blocks [0,%zu) may still be written" message correctly states). An operator debugging an incident who reads only done=5/10 from the first half of the log might incorrectly assume 5 blocks completed safely. Consider renaming to issued=%zu/%zu or adding a clarifying inline note like done(issued)= to avoid this ambiguity.


🤖 Generated by Qoder

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the semantics — for Mooncake those blocks are issued, not completed, since the RDMA may still be in flight at return.

The log line no longer claims otherwise: LogSoftTimeout states plainly that already-issued blocks may still be written, which is the point a reader needs, and I removed the surrounding duplicate warnings that were restating the same thing with a misleading count. The issued vs completed distinction is also what makes Mooncake soft in the履約 matrix.

Comment thread kv_cache_manager/client/src/internal/sdk/hf3fs_usrbio_client.cc Outdated
@github-actions github-actions Bot added the ai reviewed AI has reviewed this PR label Aug 7, 2026
#endif
}

// 验收 1(01-contract.md §6 第1条 + 11-W1 §4.1):已过期的 deadline 必须让 Get

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不要泄露决策过程

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and I did a full-PR sweep for this category in dadff14 rather than only the flagged lines. Comments that narrated why I chose something are gone; what's left states the invariant. Concrete example — deadline_util.h went from a 4-line justification to:

// deadline_ms:绝对时间点(steady_clock 毫秒),0 = 无 deadline。

Where a comment was arguing a fact (the Python/C++ clock identity), I replaced the argument with a test.

FreeBuffers(buffers);
}

// 验收 2(可选但推荐):很短但未过期的 deadline,构造"搬了一部分就超时"的中间态。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

莫名其妙的注释

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. That whole family of comments — restating what the next line plainly does, or referencing task-card documents that don't exist upstream (15-W5-tests.md, 00-context.md, connector-dataflow.md) — is cleaned out in dadff14. Verification greps over the diff for those references come back empty.

@@ -0,0 +1,575 @@
// W5 验收测试:可控 slow/fake SDK 覆盖超时与 buffer 生命周期契约

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

类似的问题

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same treatment applied here, and swept across the PR rather than spot-fixed. Also removed stale 契约 §N pointers that were wrong as well as noisy — the committed contract doc has only sections 1-4, so §6 referred to nothing at all.

Comment thread kv_cache_manager/client/src/internal/sdk/hf3fs_sdk.cc Outdated
Comment thread kv_cache_manager/client/src/internal/sdk/hf3fs_usrbio_client.cc
Comment thread kv_cache_manager/client/src/internal/sdk/sdk_wrapper.cc Outdated
Comment thread docs/design/client_sdk_io_contract.md
// - 绝对时间点,单位微秒,基于 std::chrono::steady_clock(内核 CLOCK_MONOTONIC)。
// - 0 表示「调用方不施加 deadline」:SDK 内部不做准入/等待限制(SdkWrapper 层
// 仍会用 wrapper 级 timeout_config 兜底,见 sdk_wrapper.cc)。
// - 时钟一致性(已实测):Python time.monotonic_ns()//1000 与 C++ steady_clock

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

加个测试来断言Python侧和C++侧的偏差很小;
如果一个事情可以用测试来fix,就不要写注释;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个测试加了么?加了就简化一下这里的注释。生产路径的注释都要保持高信息密度。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is added now, so the comment is gone — dadff14.

New test (in SdkTimeoutContractTest, i.e. off the production path as you asked):

[ RUN      ] SdkTimeoutContractTest.TestPythonAndCppShareSteadyClock
[       OK ] SdkTimeoutContractTest.TestPythonAndCppShareSteadyClock (10 ms)

The header dropped from 4 comment lines to 1:

// deadline_ms:绝对时间点(steady_clock 毫秒),0 = 无 deadline。

The clock-identity claim used to be argued in prose; it is now asserted by a test, which is the right place for it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b41ca3624a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return transfer_client_->LoadKvCaches(uri_str_vec, block_buffers);
// ManagerClient 层不感知 deadline(契约见 PLAN 2.2):传 0,由 SdkWrapper 级
// timeout_config 兜底(等价 PR280 之前行为)。
return transfer_client_->LoadKvCaches(uri_str_vec, block_buffers, /*deadline_us=*/0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate a real deadline for ManagerClient I/O

Fresh evidence in this revision is that the ManagerClient/RTP path explicitly passes 0 here, which disables the new backend-level deadline checks even though SdkWrapper still returns at its fallback timeout_config deadline. For callers using this public API, a LocalFile/HF3FS task can continue after ER_SDK_TIMEOUT and keep reading or writing the caller-owned buffer, recreating the corruption window this change is meant to close; compute and pass the wrapper fallback deadline down instead of 0 for these calls.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional, and unchanged: ManagerClientImpl exposes no deadline to its callers, so passing 0 preserves exactly the pre-PR behaviour for that path — the static timeout_config budget still applies inside SdkWrapper, which continues to return at its internal deadline. Nothing is disabled that used to be enabled; there was never a caller-supplied deadline here.

Inventing a deadline at this layer would mean fabricating a buffer-lifetime promise on behalf of a caller who never made one. Since the contract is "whoever owns the buffer sets the deadline", and this layer doesn't own it, 0 is the correct value.

With deadline_ms now defaulting to 0 in the header this is also no longer visible as an explicit magic argument:

// 本层不暴露 deadline:沿用 client 配置的静态超时预算。
return transfer_client_->LoadKvCaches(uri_str_vec, block_buffers);


// submit_io_count 达到最大或者没得读
if (!WaitIos(handle->ior_handle, submit_io_count)) {
if (!WaitIos(handle->ior_handle, submit_io_count, deadline_us, /*for_read=*/true)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Thread deadline_us through HF3FS helpers

In HF3FS-enabled builds, this new deadline_us argument is referenced inside ReadFrom3FS, but the helper signature still does not receive it and DoRead still calls ReadFrom3FS(handle, segments). The same pattern appears in WriteTo3FS, so any build with ENABLE_HF3FS fails before the timeout path can be tested; pass deadline_us through both helper declarations, definitions, and call sites.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

竟然是真的,这是怎么编译通过的?

Comment on lines +491 to +494
lease_deadline_us = (
time.monotonic_ns() // 1000 + int(self.write_timeout_seconds * 1_000_000)
if result is not None
else 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Don’t send absolute monotonic deadlines across hosts

When TP ranks or scheduler/worker run on different hosts, this broadcasts rank 0’s time.monotonic_ns() absolute timestamp and later compares it with another process’s local steady clock when building the SaveKvCaches deadline. Monotonic-clock epochs are only meaningful on the host that produced them, so a multi-node job can see the lease as already expired (dropping writes) or far in the future (writing after KVCM may reclaim the URI); broadcast a relative remaining lease duration or recompute a local absolute deadline on each receiver.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and no absolute deadline crosses a host or process boundary anywhere in this PR any more.

The broadcast is removed; each worker computes its own deadline from its own clock and its own config. Note the originally suggested fix — broadcasting a relative remaining time — was rejected for a different reason: every worker already knows the config, so transmitting it saves nothing while still coupling processes.

The trade-off is explicit and documented: cross-machine clock skew would break the happy path (a skewed worker deciding everything is already expired and refusing all I/O), whereas lease overrun only manifests in the timeout path. Choosing per-process computation accepts the latter to eliminate the former, and it's recorded in Known Limitations.

Comment thread kv_cache_manager/client/src/internal/sdk/sdk_wrapper.cc
# 绝不可打破 —— 与 worker 侧的 DDL_自律取 min 后才是最终 deadline)。
# 时钟源:time.monotonic_ns() 与 C++ steady_clock 同为 CLOCK_MONOTONIC(已实测),
# 直接传 us 数值比较,无需校正。
lease_deadline_us = time.monotonic_ns() // 1000 + int(self._write_timeout_seconds * 1_000_000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the server lease expiry for write deadlines

This computes the write lease deadline from local time after start_write_cache returns, but the manager starts the session timeout when WriteLocationManager::Put runs during StartWriteCache and also caps the requested timeout. When start_write_cache is slow or the configured timeout exceeds the server cap, the client can pass a SaveKvCaches deadline that is later than the real lease, allowing writes after KVCM has expired and reclaimed the locations; have the server return the authoritative expiry/remaining TTL or subtract the elapsed/capped duration before passing the deadline.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没听懂codex在说什么

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

解释一下?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这是已知的,我们暂时不修改kvcm server端的行为,我们知道这里有gap,但是我们无法完全解决这个问题;客户端怎么可能完全知道kvcm从什么时候开始计数?通信总是需要时间的,所以这是被接受的;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, treating it as accepted and unchanged. No client-side attempt to model when KVCM starts counting.

I've recorded it in docs/design/client_sdk_io_contract.md under Known Limitations so the residual gap is explicit for future readers rather than folklore: the client's deadline is anchored to its own clock at the start_write_cache response, so the RPC round-trip is inherently outside the client's knowledge, and the lease can be marginally exceeded by that amount.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair — that thread was about whether a deadline computed in the scheduler stays valid once it reaches the worker. It's moot now: no deadline crosses a process boundary in this PR, each process computes its own from its own clock, and the cross-machine clock caveat is recorded in Known Limitations. Nothing left to act on here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short version: the concern was that the deadline was being computed too early relative to where the buffer is actually used, so queueing/blocking time got charged against the SDK's transfer budget.

That's now fixed on both paths, in the two different ways their constraints demand:

  • Read: computed inside vllm/data_transfer.load_task, right after alloc_buffer_idx_blocking returns — the clock starts when the buffer being protected exists, so pool waiting no longer eats the budget.
  • Write: still computed in the worker before task submission, because it must stay anchored to the KVCM lease (start_write_cache), not to buffer acquisition — save_task blocks on kvcache_ready_event.wait() first, and starting the clock after that would drop the lease constraint entirely.

Hence the deliberately asymmetric signatures: load_task takes no deadline_ms, save_task does.

Comment thread kv_cache_manager/client/src/internal/sdk/deadline_util.h Outdated
}
std::shared_ptr<SdkInterface> sdk;
switch (type) {
auto creator_it = custom_creators_.find(type);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试代码不要耦合到生产函数路径里面,没别的办法写出不耦合的版本么?除非,生产路径会有custom_creators么?如果没有,就不要放进来。

@lpdink lpdink Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

考虑在测试代码里覆写/装饰CreateSdk?方法可以很灵活,而不是把测试相关的代码放到生产路径。

using SdkCreator = std::function<std::shared_ptr<SdkInterface>(
const std::shared_ptr<SdkBackendConfig> &, const std::shared_ptr<StorageConfig> &)>;
void RegisterCustomCreatorForTest(DataStorageType type, SdkCreator creator);
void ClearCustomCreatorsForTest();

@lpdink lpdink Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不应该为测试方便而在生产路径增加方法;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. No test-only surface remains on the production path.

Specifically dropped: the sdk_factory test-registration hook, plus the SdkDeadline / SdkIoStats / TimedTask types that existed largely to make assertions convenient. Fault injection is now done the ordinary way — a virtual method overridden by a test subclass (fake_slow_sdk.h, which lives under test/).

auto &group = groups[uri.GetPath()];
// 记录原始下标:下标是 block 的唯一身份,消费方(如 Put 回填 actual_remote_uris)
// 必须按 indices 保序,禁止依赖 unordered_map 迭代序。
group.indices.push_back(i);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

奇怪,怎么带进这个提交里了,谁修的?和本提交无关,已经在 #276 中修掉了;难怪有冲突,先rebase一下origin/main吧。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and this turned out to be the most serious problem in the branch. Fixed by rebuilding it from origin/main.

What had happened: an intermediate rebase applied a diff computed against the old fork point onto a newer main, which reverse-applied work that was already merged — #244 (GC), #272, and #259/#264, about 3353 lines. Your comment about the conflicts being suspicious was the thread that unravelled it.

Recovery was a clean branch from the latest origin/main plus a whitelisted, file-by-file replay of only this PR's changes, adjudicating each hunk individually (down to splitting a single line in docs/README.md that mixed "add contract index" with "delete GC index").

The diff is now confined to five areas — kv_cache_manager/client, kv_cache_manager/py_connector, open_source/kv_cache_manager, docs/design, docs/README.md — and I check this mechanically before每 push: any path under manager/, meta/, optimizer/, service/, metrics/, protocol/, package/ or integration_test/ means something was reverted by accident. Current run: no out-of-scope paths, and no production function or test from main missing.

// 保序契约:下标是 block 的唯一身份 —— block_keys[i] ↔ locations[i] ↔ buffers[i] ↔
// actual_uris[i] 必须一一对应。实现方(如 LocalFileSdk::Put)分组处理后必须按 indices
// 回填实际位置,禁止依赖 unordered_map 的迭代序(见 docs/design/client_sdk_io_contract.md)。
std::vector<size_t> indices;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不是本PR的任务。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed from this PR — the diff is now restricted to client, py_connector, open_source/kv_cache_manager, docs/design and docs/README.md, and I verify that path whitelist before pushing. Anything else that crept in was collateral from the bad rebase described in the #276 thread, and is gone.

Comment thread kv_cache_manager/client/src/internal/sdk/sdk_interface.h

// 尝试获取剩余毫秒。返回 true 表示有剩余(不存在 deadline 或不存在未过期时返回 false)。
// 调用方仅在返回 true 时使用 out 值(剩余毫秒,>0)。
inline bool TryRemainingMs(int64_t deadline_ms, int64_t &out) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我们在这里没有很好的办法,怎么写怎么不对,上一个版本是用了int64直接作为返回值,约定-1超时,0无限,+有限,也很反模式,现在这个写法,也行吧。fine; 最C++ style的是什么?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and changed it, because your earlier point about the two-meanings-of-false bug convinced me it was worth it:

inline std::optional<int64_t> RemainingMs(int64_t deadline_ms);

To answer the question directly — std::optional is the idiomatic choice here, and the ranking is roughly:

  1. std::optional<int64_t> — "a value, or nothing". No sentinel to memorise, and .value_or(0) expresses "treat absence as expired" at the one call site that wants that. What it does not give you is a reason for the absence.
  2. std::expected<int64_t, DeadlineState> (C++23) — would be the most precise: absence carries why (kNoDeadline vs kExpired). This is precisely the distinction whose collapse caused the HF3FS unbounded-wait bug. Not available to us on C++17, and honestly overkill for a two-state enum.
  3. std::chrono::milliseconds instead of raw int64_t — type-safe against unit mix-ups, which this codebase has already been bitten by (an earlier iteration of this PR shipped a DeadlineFromNowMs that multiplied by 1000). I did not do it here because the value crosses the pybind boundary as a plain integer, and wrapping only the internal half would just add conversions.
  4. The bool + out-param it used to be — worst of the four: the failure case is unlabelled, so callers guess.
  5. Magic int64_t sentinels (-1 timeout / 0 infinite) — the previous version, agreed it was an anti-pattern.

So: optional today, expected if we ever move to C++23. Note the "which kind of absence" question does not disappear with optional — I kept DeadlineExpired() alongside it for the caller that genuinely needs to tell the two apart, and documented that pairing in one line.

if (!ReadFrom3FS(handle, segments, deadline_ms)) {
// Deadline 过期导致 WaitIos 超时 → 3FS 可能仍有 I/O 在飞。
// 释放 iov/ior 会导致已提交的 3FS 请求写已释放内存(UAF)。
// 泄漏优于 UAF,详见 known limitations。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3fs真的没有取消语义么。。。如果有,就不会陷入这个问题了。
这里比较危险的是完全失效,这在宕机场景是有完全可能的,对于这种staging方案,我们此前的方案是维护一个TTL,例如可配置的100s,从而避免极端场景下,3fs后端完全失效。似乎没有在这个提交中落地,也许要考虑一下。这可以缓解3fs完全失效的问题...但是又引入了可能的,因为TTL 100s是拍出来的,我们不知道会不会脏数据。这要决策一下。从另一个角度讲,实际上,如果iov全丢了,应该重启该推理引擎来规避,而不是脏数据,也说得通;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不处理这个意见,我和maintainer讨论一下。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one as-is per your follow-up — happy to revisit once you've talked to the maintainer.

For the record on the narrow factual question: usrbio exposes no cancel/abort for a submitted IO, so once hf3fs_prep_io + hf3fs_submit_ios have happened there is no way to revoke the in-flight write. Hence leaking the iov/IOR on timeout rather than destroying them — bounded leak over a use-after-free into the caller's pinned buffer. HF3FS is therefore declared soft in the履约 matrix, and this is written up in Known Limitations rather than glossed over. A TTL-based reclaim like the one you describe would fit on top of that without changing the SDK contract.

One related bug in this area was real and is fixed in dadff14: an already-expired deadline used to pass a null abs_timeout to hf3fs_wait_for_ios, which means wait-forever — so the very case where we most wanted to bail out was the one that hung. Details in my reply to codex on that thread.

Comment thread kv_cache_manager/py_connector/common/utils.py
Comment thread kv_cache_manager/py_connector/vllm/v1_connector.py
# 读路径无租约:deadline 只含自律项(DDL_自律 = T_submit + sdk_get_timeout_ms)。
# T_submit = 任务提交到 io_executor 的时刻(传输线程池可能排队,deadline 须在
# 提交前算好)。time.monotonic_ns() 与 C++ steady_clock 同为 CLOCK_MONOTONIC。
load_deadline_ms = deadline_ms_from_now(self._extra_config.sdk_get_timeout_ms)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

读路径不应该提前算,读路径的风险不在和KVCM的契约,而是在超时以后,storage sdk仍然写pinned;
而这个buffer是在transfer client里面申请的。不要在这里算,这平白无故地缩短了读路径的超时时间。如果池子的task排队了的话。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

因为,只有申请了buffer的人才关心他们的buffer有没有被污染,VLLM connector没有申请buffer,它不应该关心这个概念;让transfer client区关心这个问题,它申请了buffer;它不希望自己的buffer被污染,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我的意思是vllm/data_transfer.py去关心。不是指C++的data transfer client;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, moved in dadff14. Your framing — the read-path risk is not the KVCM contract but the storage SDK still writing into pinned memory after the timeout — is the correct one, and computing the deadline here was wasting budget for no benefit.

The connector no longer computes anything; those lines are simply deleted. The deadline is now taken inside data_transfer.load_task, immediately after the buffer is in hand:

copy_buffer_indices = self._copy_buffer_allocator.alloc_buffer_idx_blocking(len(remote_uris))
copy_buffers = self._copy_buffer_allocator.get_buffer_by_idx(copy_buffer_indices)
# buffer 到手后才起算:本函数是 buffer 的所有者,deadline 之后可安全重用它们。
deadline_ms = deadline_ms_from_now(self._extra_config.sdk_get_timeout_ms)

This matters concretely because alloc_buffer_idx_blocking blocks: under pool pressure the old placement charged both the executor queueing delay and the buffer wait against the SDK's 15s, so a busy instance would hand the SDK a budget already partly spent, or even an expired one. Now the clock starts when the resource being protected actually exists.

v1_connector.py has exactly one deadline_ms_from_now call left, on the write path, for the reasons in your other comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the cleanest statement of the ownership rule and I've made the code follow it: whoever allocates the buffer is the only party that needs to reason about it being touched.

The vLLM connector allocates nothing on the read path, so it now says nothing about deadlines there — the parameter is gone from load_task's signature rather than being threaded through as 0. The owner (data_transfer.load_task) computes its own.

It also makes the layering honest: the deadline is a property of the buffer's lifetime, so it belongs with the code that controls that lifetime. Passing it down from a layer that never owned the memory was the thing that felt off.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood — kv_cache_manager/py_connector/vllm/data_transfer.py, the Python module, not the C++ TransferClient. That is where it now lives (DataTransferManager.load_task), since that function is what calls CopyBufferAllocator and therefore owns the buffer.

No C++ signature changed for this; LoadKvCaches still just receives whatever absolute deadline its caller decided on.

end_idx = min(len(blocks_idx), i + per_task_size)
task_remote_uris = all_remote_uris[i:end_idx]
task_block_token_indices = blocks_idx[i:end_idx]
deadline_ms = deadline_ms_from_now(self._extra_config.sdk_put_timeout_ms)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

写路径就是另外一回事了,由于有和KVCM之间的30s契约,我们必须在尽可能靠近self._manager_client.start_write_cache(request)的地方算DDL,同时,必须在worker上,避免多机问题。这里是最近的吗?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of your constraints hold, and I moved it slightly closer in dadff14 — though not as close as start_write_cache itself.

Where it is now: still in _build_save_tasks (worker process, satisfying constraint 2), but hoisted out of the per-task loop so it is computed once instead of drifting later for each subsequent task:

# 写路径受 KVCM 租约约束,DDL 须尽可能靠近 start_write_cache 的响应时刻,
# 且必须在 worker 进程内计算(避免跨进程时钟问题)。
deadline_ms = deadline_ms_from_now(self._extra_config.sdk_put_timeout_ms)
for i in range(0, len(blocks_idx), per_task_size):
    ...

Why not push it into save_task the way I did for the read path: save_task begins with kvcache_ready_event.wait(), i.e. it blocks until the GPU has finished producing the KV cache. Starting the clock after that wait would discard the lease constraint entirely — the lease is already ticking on the KVCM side regardless of whether we are still waiting on compute. So the write path deliberately keeps the earlier, more pessimistic anchor.

Why not right next to start_write_cache: that call happens in the scheduler process, and its result reaches the worker through connector metadata. Computing there and shipping the value across would violate constraint 1 and reintroduce the cross-process clock hazard you flagged on the sglang path. So the worker recomputing it from its own clock is the closest we can get without crossing a process boundary — the residual gap (scheduler->worker hop) is bounded by the metadata round-trip and is the known limitation documented in the design doc.


def __repr__(self):
return f"TairKvCacheConnectorMetadata(requests={self.requests})"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不要带入没有用的代码格式化。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in dadff14metadata.py is now byte-identical to origin/main.

It was pure noise (a docstring indent shift) and it had also silently dropped the trailing newline, which made the file end with \ No newline at end of file. Sorry for the review overhead.

return self._io_executor.submit(func, *args, **kwargs)

def load_task(self, multi_result: MultiResult, task_idx, remote_uris, block_token_indices):
def load_task(self, multi_result: MultiResult, task_idx, remote_uris, block_token_indices, deadline_ms):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

也就是在这个函数里面算,不要有参数对称强迫症;这个抽象在vllm包内部,完全是可以接受的。在buffer申请下来以后算。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done exactly that in dadff14 — computed inside the function, after the buffer is acquired, and no symmetry for symmetry's sake.

load_task lost the parameter outright:

def load_task(self, multi_result, task_idx, remote_uris, block_token_indices):

while save_task keeps receiving deadline_ms from the caller, because on the write path the deadline is anchored to the start_write_cache response rather than to the buffer. The two signatures are deliberately asymmetric now — they encode two genuinely different constraints, and this abstraction is private to the vllm package anyway.

Comment thread kv_cache_manager/py_connector/trtllm/connector.py
save_indices = self._parse_block_mask(block_mask, len(block_keys))
assert len(store_locations) == len(save_indices)

metadata.save.append((store_locations, [block_ids[block_pos]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

回退一下代码格式化,给review带来了负担。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in dadff14 — the metadata.save.append(...) line wrapping is back to its original form.

I also removed a now-dead import time from this file (leftover from before it switched to deadline_ms_from_now), so the remaining diff here is only the deadline change itself.

Comment thread kv_cache_manager/py_connector/trtllm/connector.py Outdated
Comment thread kv_cache_manager/client/src/internal/sdk/sdk_wrapper.cc Outdated

int timeout_ms = wrapper_config_->timeout_config().put_timeout_ms();
ec = RunWithTimeoutParallel(OpType::PUT, std::move(tasks), timeout_ms);
ec = RunWithTimeoutParallel(OpType::PUT, std::move(tasks), deadline, timeout_ms);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里为什么既有deadline,又有timeout_ms啊,删掉timeout_ms,我不理解,它看起来只被用于计算start了,用get now,可以不?为什么要用ddl和timeout_ms去计算start呀。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right on both counts — deleted in dadff14, and now() is not just "acceptable", it's the only correct option.

The old line was:

auto start = deadline - std::chrono::milliseconds(timeout_ms);

which reconstructs the start time by walking backwards from the deadline. That was already fragile, and this PR broke it outright: once deadline can be the caller's value (because we take the min), subtracting our own static budget yields a start that never happened. Every elapsed_ms computed from it — the timing in the warn logs — was fiction, understating the elapsed time by exactly however much the caller's deadline was tighter than ours.

Now:

ClientErrorCode SdkWrapper::RunWithTimeoutParallel(OpType op_type,
                                                   std::vector<std::function<ClientErrorCode()>> &&tasks,
                                                   std::chrono::steady_clock::time_point deadline) const {
    ...
    const auto start = std::chrono::steady_clock::now();

timeout_ms had no other use in the function, so the parameter is gone rather than left unused. One fewer argument, and the elapsed times in the logs are now real.

…restore tests, simplify APIs

Review fixes on top of the deadline_ms passthrough change.

Correctness:
- hf3fs_usrbio_client: an expired deadline used to pass a null abs_timeout to
  hf3fs_wait_for_ios, turning the "already out of time" case into an unbounded
  wait. Now distinguishes "no deadline" (null, legacy behaviour) from "expired"
  (zero timeout, return immediately).
- sdk_wrapper: RunWithTimeoutParallel derived its start time by subtracting the
  static budget from the deadline, which is wrong once the deadline comes from
  the caller. Drop the redundant timeout_ms parameter and read the clock.

Restored tests dropped by mistake (all from #276, verified to run):
- Hf3fsSdkTest.PutBatch_ReturnOk_InterleavedPathsPreserveOrder
- LocalFileSdkTest.TestPutMultiPathInterleavedOrdering
- TransferClientMultiStorageTest.TestSaveLoadInterleavedMultiPathOrdering

API/usability:
- deadline_ms defaults to 0 in both the C++ declaration and the pybind
  signature, so existing callers need no change.
- deadline_util: TryRemainingMs -> RemainingMs returning std::optional.

vLLM connector:
- Read path computes its deadline inside data_transfer.load_task, right after
  the copy buffer is acquired: only the buffer owner cares about the buffer
  being touched, and computing it earlier would silently spend the budget on
  pool queueing. Write path keeps computing it in the worker as close to the
  start_write_cache response as possible, now hoisted out of the task loop.

Also: new test asserting Python time.monotonic_ns() and C++ SteadyClockMs()
share one clock, reverted unrelated reformatting, and dropped comments that
referenced unpublished documents or stale section numbers.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dadff14262

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 131 to 132
py::arg("deadline_ms") = 0,
py::arg("trace_info") = nullptr,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve positional trace_info calls

Fresh evidence here is that the new defaulted deadline_ms is inserted before the existing trace_info parameter. Python callers that previously used the old three-positional form LoadKvCaches(uris, buffers, trace_info) (and similarly SaveKvCaches) now have that object bound to deadline_ms, so pybind raises a TypeError before I/O starts; add an overload with the old positional order or make the deadline keyword-only to keep the public API compatible.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有道理,改一下。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6efd820. You're right that inserting deadline_ms before trace_info silently rebinds the old positional form.

Rather than an overload or keyword-only, I moved deadline_ms to the end of the parameter list, after trace_info, in all three places (C++ declaration, transfer_client_impl, pybind):

virtual ClientErrorCode LoadKvCaches(const UriStrVec &uri_str_vec,
                                     const BlockBuffers &block_buffers,
                                     std::shared_ptr<TransferTraceInfo> trace_info = nullptr,
                                     int64_t deadline_ms = 0) = 0;
py::arg("trace_info") = nullptr,
py::arg("deadline_ms") = 0,

Both parameters keep defaults, so every legacy positional arity is preserved: LoadKvCaches(uris, buffers), LoadKvCaches(uris, buffers, trace_info), and the new LoadKvCaches(uris, buffers, trace_info, deadline_ms) all bind correctly.

The concrete proof: transfer_client_py_test.py had (in my previous revision) been changed to call LoadKvCaches(uris, buffers, 0). Under this fix I reverted it to the original LoadKvCaches(uris, buffers) from origin/main — byte-identical — and it passes untouched. In-repo connectors that do supply a deadline now pass it by keyword (deadline_ms=...) so ordering can never bite again.

This surfaced under --config=client (which builds the pybind test); it's exactly the class of thing the multi-config check now catches before review.

…l compatibility

codex flagged that inserting the defaulted deadline_ms before the existing
trace_info parameter silently rebinds it: an out-of-tree caller using the old
positional form LoadKvCaches(uris, buffers, trace_info) would bind the
TransferTraceInfo to deadline_ms and raise a pybind TypeError before any I/O.

Move deadline_ms to the end of the parameter list (after trace_info) in the
C++ declaration, the impl, and the pybind signature, so both parameters keep
default values and the legacy positional arity is unaffected. In-repo Python
callers now pass deadline_ms as a keyword; transfer_client_py_test reverts to
the original two-argument form, which is itself the compatibility proof.
@lpdink lpdink changed the title [client] SDK timeout cancellation and buffer lifecycle contract [client][connector] Pass an explicit deadline through the KV cache I/O path Aug 13, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6efd820ca0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


for (size_t i = 0; i < groups.size(); ++i) {
const auto &group = groups[i];
tasks.push_back([group, deadline_ms]() { return group.sdk->Get(group.uris, group.buffers, deadline_ms); });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the effective deadline into backend tasks

When callers omit deadline_ms or pass one later than the configured wrapper timeout, this closure still forwards the original unbounded/later value to the backend even though RunWithTimeoutParallel returns at the earlier deadline computed just above. Fresh evidence is that the wrapper now computes the min of the static timeout and caller deadline, but the task ignores that effective deadline; in default public calls that rely on timeout_config, LocalFile/HF3FS/Mooncake therefore keep seeing no/later deadline and can continue per-block/key I/O after the wrapper has returned ER_SDK_TIMEOUT, letting callers reuse buffers while backend work is still accessing them. Convert the effective deadline back to steady-clock milliseconds and pass that to both Get and Put tasks.

Useful? React with 👍 / 👎.

buffers.append(buffer)
logger.debug("start transfer")
transfer_result = self._transfer_client.LoadKvCaches(remote_uris, buffers)
transfer_result = self._transfer_client.LoadKvCaches(remote_uris, buffers, deadline_ms=deadline_ms)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Quarantine vLLM copy buffers after Mooncake timeouts

When this call returns ER_SDK_TIMEOUT from a Mooncake backend, the backend's soft contract still allows earlier per-key RDMA to be writing into the supplied buffers; this function then skips scatter but unconditionally returns copy_buffer_indices to the allocator below. In vLLM deployments using Mooncake, a later task can reuse those CPU copy buffers while stale RDMA is still active, corrupting the next transfer despite the new deadline. On soft-backend timeouts, keep these buffers quarantined until the backend is known safe or avoid issuing soft timeouts on this path.

Useful? React with 👍 / 👎.

Resolve conflicts between deadline plumbing (#280) and main:
- sdk_factory.h: keep virtual CreateSdk (test fake injection) and add
  private static CreateSdkInstance from #293 (TairMempool SSD split)
- sdk/test/BUILD: keep both SdkFactoryTest and SdkTimeoutContractTest
- sdk_wrapper_test.cc: union of includes (shm tests from #288 + deadline
  tests from #280)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b59ba2ee92

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const BlockBuffers &block_buffers,
std::shared_ptr<TransferTraceInfo> trace_info = nullptr) = 0;
std::shared_ptr<TransferTraceInfo> trace_info = nullptr,
int64_t deadline_ms = 0) = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the existing TransferClient virtual ABI

When an existing C++ client loads the updated public kv_cache_manager_client.so without being recompiled, its virtual call still passes only the old arguments; default arguments are applied only during compilation, so the new implementation reads an unspecified register as deadline_ms and may reject I/O immediately or use an arbitrary deadline. The same ABI mismatch affects SaveKvCaches; retain the old virtual entry points and add deadline-aware overloads, or otherwise require and document a coordinated rebuild.

Useful? React with 👍 / 👎.

lpdink added 3 commits August 25, 2026 13:41
…rallel

Dropping the shared stop flag and the drain loop together (3478d60)
silently widened the "writes into caller buffer after return" window
from the timeout path to the plain-error path:

- on a plain error (deadline NOT yet reached) the wrapper returned
  immediately while in-flight peer groups kept doing I/O into the
  caller's buffers; the caller typically reuses/frees buffers on
  error return, racing the in-flight DMA of another (hard) backend;
- groups still queued behind the failed one were no longer
  intercepted by anything: the time-based admission in the wrapped
  task only fires once the deadline has passed, so they started
  fresh I/O after the caller had already gotten its error back.

origin/main guarded both with stop + wait_until(deadline) (a bounded
drain); the timeout path keeps its immediate-return semantics because
at that point the deadline has passed and wait_until is a no-op.

Restore that structure, keeping the deadline-based wait and logs:

- wrapped task: short-circuit on stop OR expired deadline;
- drain(from): set stop (queued tasks never start I/O) and wait the
  remaining futures at most until the deadline;
- error path drains before returning the error; timeout path drains
  too (no-op wait) for the stop side effect.

Tests (SdkTimeoutContractTest, all timing-loose, no sleep-sync):
- TestErrorPathWaitsForInFlightPeer: fast-failing group + 400ms peer
  with a 3s deadline -> error is returned only after the peer
  finished (elapsed >= 350ms);
- TestErrorPathDrainIsBoundedByDeadline: peer sleeps 3s, deadline
  150ms -> return happens at the deadline, never at the peer's
  completion;
- TestErrorPathStopsQueuedTasks: single pool thread + blocker, three
  queued groups (err -> 500ms gap -> tail); the gap task guarantees
  the stop flag is observed before tail is ever picked up, so tail's
  I/O provably never starts (get_call_count == 0) with ~9s left on
  the deadline (i.e. the time-based admission cannot be what stopped
  it).

Also update contract doc section 4.4, which previously described the
error-path no-drain behaviour as a pre-existing origin/main issue
(it was not: main drained peers on error), and fix the misleading
comment in TestRunningTimeoutBoundedReturn that equated any drain
with an unbounded wait.
SdkWrapper::Get/Put computed min(internal budget, caller deadline) for
their own wait bound but handed the RAW caller deadline to the SDK
tasks. The two could diverge in both directions:

- caller deadline later than the internal budget: the wrapper times
  out and returns at the internal budget while background tasks keep
  doing I/O into the caller's buffers until the (later) caller
  deadline -- the exact "writes into caller buffer after return"
  window this PR exists to close, just with the wait bound and the
  admission check decoupled;
- caller deadline == 0 ("unspecified", the legacy default): SDKs
  received 0, so DeadlineExpired() is never true and every per-block
  / per-key admission check was inert; the "falls back to the static
  client timeout budget" promise in transfer_client.h only held at
  the wait layer, not at the buffer layer.

Thread the effective deadline (SteadyClockMs-based, same clock as
deadline_util.h) into the SDK tasks and derive the wait-bound
time_point from the same value, so the wrapper's wait ceiling and the
SDK's admission reference are always the same instant. deadline_ms=0
now means "the internal budget is the deadline" at every layer, which
is what the interface docs already claimed.

In-tree connectors are unaffected in practice: they pass
sdk_get/put_timeout_ms, the same source the internal budget comes
from, so min == caller.

Tests:
- TestSdkReceivesMinOfInternalAndCallerDeadline: internal budget
  300ms, caller 100000ms -> SDK observes exactly now+300;
- TestZeroCallerDeadlineStillPassesInternalBudget: caller 0 -> SDK
  observes a real (non-zero) deadline equal to the internal budget,
  proving per-block admission is armed for legacy callers;
- TestDeadlinePropagation now uses a caller deadline clearly below
  the fixture's internal budget, so "SDK sees exactly the caller's
  value" is the tested min semantics instead of an artifact of the
  caller timestamp being taken first.

Contract doc section 2 updated to state that the min result is what
is handed to the SDKs.
…e fails

The invariant documented right above the branch says an expired
deadline must turn into a zero timeout ("passing nullptr becomes an
unbounded wait"), yet the clock_gettime(CLOCK_REALTIME) failure branch
did exactly that: abs_timeout_ptr = nullptr, i.e. hf3fs_wait_for_ios
waits forever while the caller is already past its deadline.

Treat a failed clock_gettime as "expired" (zero timeout) so a bounded
caller stays bounded on every path. clock_gettime(CLOCK_REALTIME) is
unconditionally supported and does not fail in practice; this is pure
defence-in-depth for the stated invariant.

Test: TestWaitIosExpiredDeadlinePassesZeroTimeoutNotNullptr pins the
zero-timeout construction (non-null abs, at/before the entry wall
clock) -- previously this shape also covered the failure branch's
nullptr fallback, which the mock would have caught as
captured_null == true.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bd2ebfde2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +104 to +105
} else {
ReleaseIovIor(handle);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve HF3FS resources on every incomplete wait

When Hf3fsWaitForIos returns before all submitted requests complete, this branch releases the iov/IOR unless a separate monotonic-clock check already reports expiration. That does not prove the requests are drained: WaitIos explicitly permits an incomplete non-timeout result, and its absolute timeout uses CLOCK_REALTIME, so a forward wall-clock adjustment can trigger it while the monotonic deadline remains unexpired. The remaining requests can then access freed resources; the same conditional release occurs on the write path. Fresh evidence in this revision is that the leak safeguard is conditioned on DeadlineExpired() rather than on whether all submitted I/O was actually completed, so preserve these resources whenever an incomplete wait may leave requests in flight.

Useful? React with 👍 / 👎.

Comment on lines +110 to 113
// 契约要求(docs/design/client_sdk_io_contract.md HF3FS 行):CopyIovs 只在读取完全成功时执行,
// 超时/部分完成/出错时一律不把半成品数据交付给 caller。
// 顺序不可调整,防止后人"优化"时把 CopyIovs 挪到失败分支之前。
CopyIovs(iovs, handle->iov_handle, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck the deadline before copying staged HF3FS reads

When the 3FS wait consumes the remaining budget but still reports all requests complete, execution reaches CopyIovs without another deadline admission check. The wrapper can therefore return ER_SDK_TIMEOUT at the deadline while this background task starts copying the staged data into the caller's CPU/GPU buffer afterward, defeating the reason HF3FS is classified as a hard backend. Check the deadline after ReadFrom3FS succeeds and before beginning the caller-buffer copy so an I/O completion at the deadline is not treated as permission to start a new transfer phase.

Useful? React with 👍 / 👎.

CHECK_SDK();
auto remote_uris = ParseLocations(uri_str_vec);
auto ec = sdk_wrapper_->Get(remote_uris, block_buffers);
auto ec = sdk_wrapper_->Get(remote_uris, block_buffers, deadline_ms);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce deadlines around SDK buffer checks

When KVCM_SDK_CHECK is enabled and the backend completes near the deadline, this call can return ER_OK and then spend additional time acquiring a check cell and hashing every caller buffer in GetBlocksHash. No deadline check surrounds that diagnostic work, so LoadKvCaches can continue reading the caller's CPU/GPU buffers and return success after the advertised absolute deadline; the pre-write hash has the same buffer-lifetime issue. Skip or bound these checks once deadline_ms expires so optional diagnostics do not violate the public I/O contract.

Useful? React with 👍 / 👎.

lpdink added 2 commits August 25, 2026 13:57
3478d60 dropped the actual_remote_uris position-correspondence
assertions from TestMultipleUriWithCpu (and its GPU twin) while
mechanically adding the deadline_ms parameter; dadff14 restored three
other accidentally-dropped tests but missed these. The same PR added a
"same-order contract" comment in mooncake_sdk.cc -- the test enforcing
it should not be missing.

Alloc is an identity assignment today, so the assertions hold; their
value is guarding future refactors of Alloc into per-item backfill
(which must preserve position correspondence by index, exactly like
LocalFileSdk::Put does).

Byte-identical to the assertions as they existed on origin/main (from
36c4875). MooncakeSdkTest is a manual target needing a live mooncake
master (localhost:50051); verified it compiles with
--config=mooncake_common.
LogSoftTimeout formatted is_get ? "get" : "put" twice (once as
"mooncake %s timeout" and once as "op=%s"), printing the same value
twice on every line. Keep the single mooncake-prefixed field.

Log format change only; callers and fields otherwise untouched.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bdbb11a592

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +687 to +689
// 逐 block 准入:deadline 已过则停止搬运,不再发起
// 后续 memcpy/async copy。若已有 GPU async copy 入队,guard 析构会在返回前同步。
if (DeadlineExpired(deadline_ms)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound the final flush before declaring a write deadline-safe

When the last LocalFile/NFS block is admitted just before the deadline, the loop can finish its memcpy and then execute the unconditional msync(..., MS_SYNC) after the wrapper has already returned ER_SDK_TIMEOUT. On a slow filesystem, that flush may continue persisting stale pages after KVCM has expired and reassigned the URI, defeating the write-lease protection added by this change; the durability phase must be included in the deadline/cancellation design rather than guarding only block admission.

Useful? React with 👍 / 👎.

lpdink added a commit that referenced this pull request Aug 25, 2026
Per-task torch.empty staging made the connector's VRAM footprint
unbounded and racing the engine for its own allocation: at high load on
low-headroom GPUs (doc085 @ gpu-mem-util 0.92) the 98 MiB per load task
allocations OOMed the engine where origin/main -- whose gather kernel
writes a pre-allocated pinned host pool directly -- ran fine.

Replace the dynamic allocs with one _StagingPool per transfer group
(save and load share it): a fixed, configurable HBM + pinned reservation
(staging_pool_blocks, default 128, validated >= the largest task batch)
handed out as contiguous runs, with blocking acquire as backpressure
when exhausted. Bulk D2H/H2D and kernel views are unchanged -- only the
buffer source differs. Exception paths drain the stream before the
slots go back so a failed task cannot leave enqueued work against a
reused view.

This also restores origin/main's no-dynamic-allocation invariant the
revert of _PinnedBudget (7bd0413) dropped, without reintroducing the
connector-level lifetime governance that #280's deadline chain owns.
lpdink added a commit that referenced this pull request Aug 25, 2026
…path

The pool fix (a0d6289) bounded the connector's staging but kept a
device-side mirror per transfer group: staging_pool_blocks x
per_block_bytes of permanently reserved HBM (112 MiB/group at the
default on Qwen2.5-7B TP1, once per hybrid group) that competes with
the engine's KV cache and batch headroom -- exactly the reviewer
concern on low-end cards. The mirror only existed to route transfers
through a bulk D2H/H2D copy, and that copy is not needed: the strided
gather/scatter kernel addresses host pinned memory directly over PCIe
(UVA zero-copy, as in origin/main), and state-group copies are plain
copy_ between the GPU tensors and the pinned slices.

Make _StagingPool pinned-host-only and delete the GPU buffer, the
gpu_view, and both bulk copies. Save gathers HBM -> pinned slot and
hands the slot to SaveKvCaches; load hands the slot to LoadKvCaches and
scatters pinned -> HBM. Backpressure, contiguous-run allocation,
exception-path stream drains and the capacity guard are unchanged; the
kernel and the scheduler/worker instruction flow are untouched.
staging_pool_blocks now sizes host RAM only.

Trade-off: a slot is reused once its task reports, so after an SDK
timeout/error a background DMA may still scribble on a reused slot --
the same exposure origin/main's CopyBufferAllocator always had. The
proper fix is #280's deadline contract; taking the exposure now is
what buys zero VRAM without delaying this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai reviewed AI has reviewed this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant